Skip to content

Fix sign-in hang and athlete positions#36

Merged
jeremylongshore merged 3 commits intomainfrom
fix/signin-hang-positions
Mar 6, 2026
Merged

Fix sign-in hang and athlete positions#36
jeremylongshore merged 3 commits intomainfrom
fix/signin-hang-positions

Conversation

@jeremylongshore
Copy link
Collaborator

@jeremylongshore jeremylongshore commented Feb 28, 2026

Summary\n- Stabilize server auth by reading cookies from NextRequest and adding timeouts + provisioning fallback.\n- Fix player update endpoint to persist primary/secondary positions and related profile fields.\n- Fix athlete add/edit photo upload to use /api/storage/upload-player-photo (correct FormData key).\n- Keep secondary positions consistent when primary position changes.\n\n## Testing\n- npm run test:unit\n- npm run lint\n

Summary by CodeRabbit

  • New Features

    • Added richer player profiles: gender, primary/secondary positions, position notes, league fields, and consistent primary/secondary management.
  • Bug Fixes

    • Improved authentication reliability with request-aware auth, timeouts, and provisioning fallbacks.
    • Strengthened photo upload and storage handling with file validation and quota checks.
  • Chores

    • Updated lint ignores for test artifacts and expanded testing/tooling documentation.

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Authentication was made request-aware across many API routes; auth/provisioning gained timeouts and request-cookie handling. Storage validation/quota logic was moved to a new isomorphic module. Player update validation/schema and admin-focused storage/player service usage were introduced; frontend photo upload endpoints updated.

Changes

Cohort / File(s) Summary
Build Configuration
eslint.config.mjs
Added ignore globs for non-production/test artifacts (vertex-agents/, 03-Tests/, test-results/**).
Auth Core
src/lib/auth.ts
Made auth/authWithProfile request-aware, pull session cookie from request when present, added timeouts, provisioning fallback (ensureUserProvisioned), and updated signatures.
API Routes — auth(request) updates
src/app/api/.../route.ts (many files)
src/app/api/account/pin/route.ts, .../billing/create-checkout-session/route.ts, .../debug/**/route.ts, .../games/route.ts, .../players/**/route.ts, .../verify/route.ts
Replaced auth() with auth(request) across GET/POST/PUT/PATCH/DELETE handlers to pass the incoming request into auth. No substantive control-flow changes beyond auth invocation.
Player APIs — validation & payload expansion
src/app/api/players/[id]/route.ts, src/app/api/players/route.ts, src/app/api/players/create/route.ts
Added server-side parsing/validation using playerSchema.safeParse() in PUT, try/catch for request.json(), expanded player payload fields (gender, primary/secondary positions, positionNote, leagueCode/leagueOtherName, teamClub), and updated GET handler signature to accept NextRequest. Switched create flow cookie read to request.cookies.get('__session').
Storage — utils and re-exports
src/lib/firebase/storage-utils.ts, src/lib/firebase/storage.ts
Created new isomorphic storage-utils.ts with STORAGE_LIMITS, ALLOWED_IMAGE_TYPES, validateFile(), extractStoragePath(), and quota helpers; refactored storage.ts to re-export these utilities and retain upload/delete functions.
Admin storage/player services
src/app/api/storage/upload-player-photo/route.ts, src/app/api/storage/delete-player-photo/route.ts, src/lib/firebase/admin-services/players.ts
Switched to admin-focused service functions (getPlayerAdmin, updatePlayerAdmin, uploadPlayerPhotoAdmin, deletePlayerPhotoAdmin, getWorkspaceByIdAdmin, updateWorkspaceStorageUsageAdmin); extended updatePlayerAdmin payload to accept birthday and position.
Frontend — Dashboard photo upload & positions
src/app/dashboard/add-athlete/page.tsx, src/app/dashboard/athletes/[id]/edit/page.tsx
Changed FormData key from "photo" to "file" and endpoint to /api/storage/upload-player-photo; primary-position change handler now removes chosen primary from secondaryPositions.
Tests
src/app/api/players/[id]/route.test.ts
Expanded PUT test payload and assertions (new fields), replaced mocks.clearAllMocks() with mocks.resetAllMocks(), and updated validation error expectation to VALIDATION_FAILED.
Docs & CI
CLAUDE.md, .github/workflows/crawl-adk-docs.yml
Updated tooling/testing docs (RN/Expo notes, expanded test commands) and disabled weekly cron in workflow (commented schedule).

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Route
  participant Auth
  participant Firebase as UserDB
  participant Provisioner

  Client->>Route: HTTP request (with cookies)
  Route->>Auth: auth(request)
  Auth->>Auth: extract session cookie from request
  Auth->>Auth: verifySessionCookie (with timeout)
  Auth->>UserDB: fetch user profile by uid (with timeout)
  alt user not found
    Auth->>Provisioner: ensureUserProvisioned(uid) (with timeout)
    Provisioner->>UserDB: create/ensure user doc
    Auth->>UserDB: fetch user profile again
  end
  Auth-->>Route: Session / DashboardUser | null
  Route-->>Client: API response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I nibbled through cookies, found the session's track,
Hopped to provision users when profiles went slack,
I stored the photos, checked quotas with a grin,
Pruned secondary hops when primaries win,
A tiny rabbit patch — now everything's back on track!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix sign-in hang and athlete positions' accurately captures the two main objectives of the pull request: stabilizing authentication to prevent sign-in hangs and fixing athlete position field persistence and handling.
Docstring Coverage ✅ Passed Docstring coverage is 81.54% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/signin-hang-positions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review
Copy link

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Review Summary by Qodo

Fix sign-in hangs with request-based auth and stabilize athlete profile updates

🐞 Bug fix ✨ Enhancement

Grey Divider

Walkthroughs

Description
• Pass NextRequest to auth() calls to read cookies directly from request instead of calling
  cookies() internally, preventing sign-in hangs
• Add timeout wrappers around Firebase operations to prevent indefinite hangs during session
  verification and user provisioning
• Implement comprehensive player profile validation using playerSchema with support for
  primary/secondary positions, gender, league code, and position notes
• Fix photo upload endpoint to use correct FormData key (file instead of photo) and correct API
  route (/api/storage/upload-player-photo)
• Prevent primary position from appearing in secondary positions list when primary position changes
• Refactor storage utilities into isomorphic storage-utils.ts module for shared client/server
  usage
• Migrate storage operations to use admin SDK functions for consistency and proper access control
Diagram
flowchart LR
  A["API Routes"] -->|"Pass NextRequest"| B["auth function"]
  B -->|"Read cookies directly"| C["Session verification"]
  C -->|"Add timeouts"| D["Prevent hangs"]
  E["Player Update"] -->|"Validate with schema"| F["Position handling"]
  F -->|"Persist all fields"| G["Firestore"]
  H["Photo Upload"] -->|"Use correct FormData key"| I["Storage API"]
  I -->|"Admin SDK"| J["Firebase Storage"]
Loading

Grey Divider

File Changes

1. src/lib/auth.ts 🐞 Bug fix +59/-11

Add request-based cookie reading and operation timeouts

src/lib/auth.ts


2. src/app/api/account/pin/route.ts 🐞 Bug fix +1/-1

Pass NextRequest to auth function

src/app/api/account/pin/route.ts


3. src/app/api/billing/create-checkout-session/route.ts 🐞 Bug fix +1/-1

Pass NextRequest to auth function

src/app/api/billing/create-checkout-session/route.ts


View more (34)
4. src/app/api/debug/biometrics/[playerId]/route.ts 🐞 Bug fix +1/-1

Pass NextRequest to auth function

src/app/api/debug/biometrics/[playerId]/route.ts


5. src/app/api/debug/workout-logs/[playerId]/route.ts 🐞 Bug fix +1/-1

Pass NextRequest to auth function

src/app/api/debug/workout-logs/[playerId]/route.ts


6. src/app/api/games/route.ts 🐞 Bug fix +2/-2

Pass NextRequest to auth function

src/app/api/games/route.ts


7. src/app/api/players/[id]/assessments/[assessmentId]/route.ts 🐞 Bug fix +3/-3

Pass NextRequest to auth function

src/app/api/players/[id]/assessments/[assessmentId]/route.ts


8. src/app/api/players/[id]/assessments/route.ts 🐞 Bug fix +2/-2

Pass NextRequest to auth function

src/app/api/players/[id]/assessments/route.ts


9. src/app/api/players/[id]/biometrics/[logId]/route.ts 🐞 Bug fix +3/-3

Pass NextRequest to auth function

src/app/api/players/[id]/biometrics/[logId]/route.ts


10. src/app/api/players/[id]/biometrics/route.ts 🐞 Bug fix +2/-2

Pass NextRequest to auth function

src/app/api/players/[id]/biometrics/route.ts


11. src/app/api/players/[id]/cardio-logs/[logId]/route.ts 🐞 Bug fix +3/-3

Pass NextRequest to auth function

src/app/api/players/[id]/cardio-logs/[logId]/route.ts


12. src/app/api/players/[id]/cardio-logs/route.ts 🐞 Bug fix +2/-2

Pass NextRequest to auth function

src/app/api/players/[id]/cardio-logs/route.ts


13. src/app/api/players/[id]/dream-gym/ai-strategy/route.ts 🐞 Bug fix +2/-2

Pass NextRequest to auth function

src/app/api/players/[id]/dream-gym/ai-strategy/route.ts


14. src/app/api/players/[id]/dream-gym/check-in/route.ts 🐞 Bug fix +1/-1

Pass NextRequest to auth function

src/app/api/players/[id]/dream-gym/check-in/route.ts


15. src/app/api/players/[id]/dream-gym/events/[eventId]/route.ts 🐞 Bug fix +1/-1

Pass NextRequest to auth function

src/app/api/players/[id]/dream-gym/events/[eventId]/route.ts


16. src/app/api/players/[id]/dream-gym/events/route.ts 🐞 Bug fix +1/-1

Pass NextRequest to auth function

src/app/api/players/[id]/dream-gym/events/route.ts


17. src/app/api/players/[id]/dream-gym/route.ts 🐞 Bug fix +2/-2

Pass NextRequest to auth function

src/app/api/players/[id]/dream-gym/route.ts


18. src/app/api/players/[id]/dream-gym/workout-logs/[logId]/route.ts 🐞 Bug fix +3/-3

Pass NextRequest to auth function

src/app/api/players/[id]/dream-gym/workout-logs/[logId]/route.ts


19. src/app/api/players/[id]/dream-gym/workout-logs/route.ts 🐞 Bug fix +2/-2

Pass NextRequest to auth function

src/app/api/players/[id]/dream-gym/workout-logs/route.ts


20. src/app/api/players/[id]/journal/[entryId]/route.ts 🐞 Bug fix +3/-3

Pass NextRequest to auth function

src/app/api/players/[id]/journal/[entryId]/route.ts


21. src/app/api/players/[id]/journal/route.ts 🐞 Bug fix +2/-2

Pass NextRequest to auth function

src/app/api/players/[id]/journal/route.ts


22. src/app/api/players/[id]/practice-logs/[logId]/route.ts 🐞 Bug fix +3/-3

Pass NextRequest to auth function

src/app/api/players/[id]/practice-logs/[logId]/route.ts


23. src/app/api/players/[id]/practice-logs/route.ts 🐞 Bug fix +2/-2

Pass NextRequest to auth function

src/app/api/players/[id]/practice-logs/route.ts


24. src/app/api/players/[id]/route.ts ✨ Enhancement +35/-11

Validate player data and persist all profile fields

src/app/api/players/[id]/route.ts


25. src/app/api/players/[id]/route.test.ts 🧪 Tests +26/-11

Update tests for new player validation schema

src/app/api/players/[id]/route.test.ts


26. src/app/api/players/create/route.ts 🐞 Bug fix +2/-4

Pass NextRequest to auth and read cookies from request

src/app/api/players/create/route.ts


27. src/app/api/players/route.ts ✨ Enhancement +10/-4

Pass NextRequest to auth and return extended player fields

src/app/api/players/route.ts


28. src/app/api/storage/delete-player-photo/route.ts ✨ Enhancement +9/-8

Migrate to admin SDK and storage-utils module

src/app/api/storage/delete-player-photo/route.ts


29. src/app/api/storage/upload-player-photo/route.ts ✨ Enhancement +11/-15

Migrate to admin SDK and storage-utils module

src/app/api/storage/upload-player-photo/route.ts


30. src/app/api/verify/route.ts 🐞 Bug fix +1/-1

Pass NextRequest to auth function

src/app/api/verify/route.ts


31. src/lib/firebase/storage-utils.ts ✨ Enhancement +128/-0

Create isomorphic storage utilities module

src/lib/firebase/storage-utils.ts


32. src/lib/firebase/storage.ts Refactoring +10/-140

Export utilities from storage-utils module

src/lib/firebase/storage.ts


33. src/lib/firebase/admin-services/players.ts ✨ Enhancement +2/-0

Add birthday and position fields to update schema

src/lib/firebase/admin-services/players.ts


34. src/app/dashboard/add-athlete/page.tsx 🐞 Bug fix +10/-3

Fix photo upload FormData key and prevent primary in secondary

src/app/dashboard/add-athlete/page.tsx


35. src/app/dashboard/athletes/[id]/edit/page.tsx 🐞 Bug fix +10/-3

Fix photo upload FormData key and prevent primary in secondary

src/app/dashboard/athletes/[id]/edit/page.tsx


36. src/app/login/page.tsx 🐞 Bug fix +5/-1

Add timeout to getIdToken operation

src/app/login/page.tsx


37. eslint.config.mjs ⚙️ Configuration changes +7/-0

Exclude generated test artifacts from linting

eslint.config.mjs


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Feb 28, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. authWithProfile not request-scoped 🐞 Bug ⛯ Reliability
Description
Multiple API routes still call authWithProfile() without passing the NextRequest, causing
authWithProfile() to fall back to cookies() from next/headers. This likely reintroduces the same
class of cookie/request-stream instability the PR is trying to fix, leaving some authenticated
endpoints prone to hangs/timeouts.
Code

src/lib/auth.ts[R52-60]

+async function getSessionCookieFromHeaders(): Promise<string | null> {
  const cookieStore = await cookies();
-  return cookieStore.get('__session')?.value || null;
+  return cookieStore.get(SESSION_COOKIE_NAME)?.value || null;
+}
+
+async function getSessionCookie(request?: NextRequest): Promise<string | null> {
+  if (request) return getSessionCookieFromRequest(request);
+  return getSessionCookieFromHeaders();
}
Evidence
authWithProfile() uses getSessionCookie(request?) which falls back to cookies() when request is
omitted; several API handlers still call authWithProfile() with no argument (and
/api/workspace/current doesn’t accept a request at all), so they cannot benefit from reading cookies
off NextRequest.

src/lib/auth.ts[52-60]
src/app/api/billing/change-plan/route.ts[32-41]
src/app/api/billing/invoices/route.ts[27-50]
src/app/api/workspace/current/route.ts[16-26]
src/app/api/admin/billing/replay-events/route.ts[106-115]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Several API routes still call `authWithProfile()` without passing the `NextRequest`, which forces auth to fall back to `cookies()` from `next/headers`. This undermines the PR’s “read cookies from NextRequest” stabilization and may leave these routes vulnerable to the same hang/instability.

## Issue Context
`authWithProfile(request?: NextRequest)` is designed to use `request.cookies` when a request is provided and fall back to `cookies()` otherwise.

## Fix Focus Areas
- src/app/api/billing/change-plan/route.ts[32-42]
- src/app/api/billing/invoices/route.ts[27-50]
- src/app/api/billing/portal/route.ts[27-50]
- src/app/api/billing/create-portal-session/route.ts[24-35]
- src/app/api/admin/billing/replay-events/route.ts[106-116]
- src/app/api/workspace/current/route.ts[10-26]
- src/lib/auth.ts[52-60]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. withTimeout timer leak 🐞 Bug ⛯ Reliability
Description
withTimeout does not clear its timeout when the wrapped promise rejects, leaving timers scheduled
until expiry. Since this PR now wraps hot-path auth/session verification calls with withTimeout,
frequent auth failures can accumulate timers and waste resources (and can keep serverless
invocations alive longer).
Code

src/lib/auth.ts[R74-78]

+    const decodedToken = await withTimeout(
+      adminAuth.verifySessionCookie(sessionCookie),
+      VERIFY_SESSION_COOKIE_TIMEOUT_MS,
+      'verifySessionCookie'
+    );
Evidence
The timeout is only cleared in the success path (promise.then(...)), not on rejection; auth() now
wraps verifySessionCookie with withTimeout, which can reject often (invalid/expired cookies),
leaving pending timers.

src/lib/utils/timeout.ts[8-17]
src/lib/auth.ts[69-78]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`withTimeout()` only clears its `setTimeout` when the wrapped promise resolves. If the wrapped promise rejects, the timer remains scheduled until it fires.

## Issue Context
This PR increases usage of `withTimeout()` in authentication flows, so the leak is more impactful.

## Fix Focus Areas
- src/lib/utils/timeout.ts[8-17]
- src/lib/auth.ts[69-79]
- src/lib/auth.ts[102-112]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Age validation inaccurate 🐞 Bug ✓ Correctness
Description
playerSchema’s birthday refinement computes age using only year difference, ignoring month/day,
which can incorrectly accept under-5 players and reject valid 25-year-olds around birthdays. Because
this PR now enforces playerSchema on PUT /api/players/[id], legitimate updates may start failing
unexpectedly.
Code

src/app/api/players/[id]/route.ts[R86-95]

+    const validationResult = playerSchema.safeParse(body);
+    if (!validationResult.success) {
+      return NextResponse.json(
+        {
+          error: 'VALIDATION_FAILED',
+          message: 'Please check the form fields and try again.',
+          details: validationResult.error.flatten().fieldErrors,
+        },
+        { status: 400 }
+      );
Evidence
The schema’s refine logic uses today.getFullYear() - birthDate.getFullYear() and enforces 5–25
based on that coarse value; the updated API route now blocks updates when this schema fails, so
boundary cases become user-visible failures.

src/lib/validations/player.ts[107-115]
src/app/api/players/[id]/route.ts[86-95]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The age calculation in `playerSchema` is year-only and can be off by one year around birthdays.

## Issue Context
The API now enforces `playerSchema` during updates, so this logic directly affects user-facing behavior.

## Fix Focus Areas
- src/lib/validations/player.ts[107-115]
- src/app/api/players/[id]/route.ts[86-99]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's stability and data integrity by addressing critical authentication issues and expanding athlete profile management capabilities. It introduces explicit request-based authentication, implements timeouts for key server-side operations to prevent sign-in hangs, and provides a more comprehensive and validated structure for athlete positions and personal details. Additionally, photo handling has been streamlined through dedicated storage APIs, and development tooling is improved with updated ESLint ignore patterns.

Highlights

  • Enhanced Server-Side Authentication: The authentication mechanism across numerous API routes has been updated to explicitly pass the NextRequest object to the auth() function, allowing for more robust cookie reading and session management directly from the request context. This change impacts almost all API endpoints requiring user authentication.
  • Improved Authentication Stability with Timeouts and Fallbacks: Critical authentication and user provisioning steps now incorporate withTimeout utilities, preventing potential hangs during getIdToken, verifySessionCookie, getUserProfile, and ensureUserProvisioned operations. A fallback provisioning logic has also been added for authWithProfile to handle cases where a user document might initially be missing.
  • Comprehensive Athlete Profile Management: Player profile updates have been significantly enhanced to support a wider range of fields, including gender, birthday, primaryPosition, secondaryPositions, positionNote, leagueCode, and leagueOtherName. A new playerSchema has been introduced for robust validation of these fields, and the position field is now explicitly set to primaryPosition for backward compatibility.
  • Streamlined Photo Upload and Deletion: Photo upload and deletion functionalities have been refactored to use dedicated /api/storage/upload-player-photo and /api/storage/delete-player-photo endpoints. These endpoints now leverage new admin-storage services and shared storage-utils for better organization and server-side control, including updated FormData keys for uploads.
  • Consistent Position Selection in Athlete Forms: The athlete add/edit forms now ensure data consistency by automatically removing a newly selected primary position from the list of secondary positions, preventing redundant or conflicting entries.
  • Updated ESLint Configuration: The ESLint configuration has been refined to ignore new directories and files, specifically vertex-agents/** (for Python environments), and various Playwright test artifacts and reports, improving linting performance and relevance.
Changelog
  • eslint.config.mjs
    • Added new ignore patterns for vertex-agents/**, Playwright reports, and test results to the ESLint configuration.
  • src/app/api/account/pin/route.ts
    • Updated auth() call to auth(request) to pass the NextRequest object for session authentication.
  • src/app/api/billing/create-checkout-session/route.ts
    • Updated auth() call to auth(request) to pass the NextRequest object for session authentication.
  • src/app/api/debug/biometrics/[playerId]/route.ts
    • Updated auth() call to auth(request) to pass the NextRequest object for session authentication.
  • src/app/api/debug/workout-logs/[playerId]/route.ts
    • Updated auth() call to auth(request) to pass the NextRequest object for session authentication.
  • src/app/api/games/route.ts
    • Updated auth() calls in both GET and POST handlers to auth(request) for session authentication.
  • src/app/api/players/[id]/assessments/[assessmentId]/route.ts
    • Updated auth() calls in GET, PUT, and DELETE handlers to auth(request) for session authentication.
  • src/app/api/players/[id]/assessments/route.ts
    • Updated auth() calls in GET and POST handlers to auth(request) for session authentication.
  • src/app/api/players/[id]/biometrics/[logId]/route.ts
    • Updated auth() calls in GET, PUT, and DELETE handlers to auth(request) for session authentication.
  • src/app/api/players/[id]/biometrics/route.ts
    • Updated auth() calls in GET and POST handlers to auth(request) for session authentication.
  • src/app/api/players/[id]/cardio-logs/[logId]/route.ts
    • Updated auth() calls in GET, PATCH, and DELETE handlers to auth(request) for session authentication.
  • src/app/api/players/[id]/cardio-logs/route.ts
    • Updated auth() calls in GET and POST handlers to auth(request) for session authentication.
  • src/app/api/players/[id]/dream-gym/ai-strategy/route.ts
    • Updated auth() calls in POST and GET handlers to auth(request) for session authentication.
  • src/app/api/players/[id]/dream-gym/check-in/route.ts
    • Updated auth() call to auth(request) to pass the NextRequest object for session authentication.
  • src/app/api/players/[id]/dream-gym/events/[eventId]/route.ts
    • Updated auth() call to auth(request) to pass the NextRequest object for session authentication.
  • src/app/api/players/[id]/dream-gym/events/route.ts
    • Updated auth() call to auth(request) to pass the NextRequest object for session authentication.
  • src/app/api/players/[id]/dream-gym/route.ts
    • Updated auth() calls in GET and POST handlers to auth(request) for session authentication.
  • src/app/api/players/[id]/dream-gym/workout-logs/[logId]/route.ts
    • Updated auth() calls in GET, PUT, and DELETE handlers to auth(request) for session authentication.
  • src/app/api/players/[id]/dream-gym/workout-logs/route.ts
    • Updated auth() calls in GET and POST handlers to auth(request) for session authentication.
  • src/app/api/players/[id]/journal/[entryId]/route.ts
    • Updated auth() calls in GET, PUT, and DELETE handlers to auth(request) for session authentication.
  • src/app/api/players/[id]/journal/route.ts
    • Updated auth() calls in GET and POST handlers to auth(request) for session authentication.
  • src/app/api/players/[id]/practice-logs/[logId]/route.ts
    • Updated auth() calls in GET, PATCH, and DELETE handlers to auth(request) for session authentication.
  • src/app/api/players/[id]/practice-logs/route.ts
    • Updated auth() calls in GET and POST handlers to auth(request) for session authentication.
  • src/app/api/players/[id]/route.test.ts
    • Updated validPutBody to include new player profile fields like gender, primaryPosition, secondaryPositions, positionNote, and leagueCode.
    • Replaced vi.clearAllMocks() with vi.resetAllMocks() in beforeEach hooks.
    • Modified validation error expectation from 'Missing required fields' to 'VALIDATION_FAILED'.
    • Expanded expect.objectContaining in updatePlayerAdmin test to verify all new player profile fields.
  • src/app/api/players/[id]/route.ts
    • Imported playerSchema for player data validation.
    • Updated auth() calls in GET, PUT, and DELETE handlers to auth(request) for session authentication.
    • Replaced manual body validation with playerSchema.safeParse(body) for comprehensive player profile validation.
    • Updated the updatePlayerAdmin call to pass all new player profile fields, including birthday, gender, primaryPosition, secondaryPositions, positionNote, leagueCode, and leagueOtherName.
  • src/app/api/players/create/route.ts
    • Updated auth() call to auth(request) to pass the NextRequest object for session authentication.
    • Modified cookie retrieval for session verification to use request.cookies.get('__session') instead of next/headers.
  • src/app/api/players/route.ts
    • Updated GET handler to accept NextRequest and pass it to auth(request).
    • Expanded the returned player object in the GET response to include gender, primaryPosition, secondaryPositions, positionNote, leagueCode, and leagueOtherName.
  • src/app/api/storage/delete-player-photo/route.ts
    • Updated auth() call to auth(request) for session authentication.
    • Replaced imports from firebase/storage and firebase/services with firebase/admin-storage and firebase/admin-services.
    • Changed function calls to use admin versions: getPlayerAdmin, deletePlayerPhotoAdmin, updatePlayerAdmin, and updateWorkspaceStorageUsageAdmin.
  • src/app/api/storage/upload-player-photo/route.ts
    • Updated auth() call to auth(request) for session authentication.
    • Replaced imports from firebase/storage and firebase/services with firebase/admin-storage, firebase/admin-services, and firebase/storage-utils.
    • Changed function calls to use admin versions: getPlayerAdmin, getWorkspaceByIdAdmin, deletePlayerPhotoAdmin, uploadPlayerPhotoAdmin, updatePlayerAdmin, and updateWorkspaceStorageUsageAdmin.
  • src/app/api/verify/route.ts
    • Updated auth() call to auth(request) to pass the NextRequest object for session authentication.
  • src/app/dashboard/add-athlete/page.tsx
    • Changed photo upload endpoint from /api/players/upload-photo to /api/storage/upload-player-photo.
    • Updated FormData key for photo upload from photo to file.
    • Modified the primaryPosition onChange handler to automatically filter the selected primary position out of the secondaryPositions array.
  • src/app/dashboard/athletes/[id]/edit/page.tsx
    • Changed photo upload endpoint from /api/players/upload-photo to /api/storage/upload-player-photo.
    • Updated FormData key for photo upload from photo to file.
    • Modified the primaryPosition onChange handler to automatically filter the selected primary position out of the secondaryPositions array.
  • src/app/login/page.tsx
    • Added withTimeout utility to the user.getIdToken() call to prevent potential hangs during token retrieval.
  • src/lib/auth.ts
    • Added NextRequest type import and withTimeout utility import.
    • Introduced constants for session cookie name and various timeout durations.
    • Implemented getSessionCookieFromRequest and getSessionCookieFromHeaders to read cookies based on NextRequest presence.
    • Modified auth() and authWithProfile() to accept an optional NextRequest parameter.
    • Wrapped adminAuth.verifySessionCookie, adminDb.collection('users').doc(decodedToken.uid).get(), and ensureUserProvisioned calls with withTimeout for improved stability.
    • Added fallback user provisioning logic within authWithProfile if a user document is not found after initial session verification.
  • src/lib/firebase/admin-services/players.ts
    • Updated the updatePlayerAdmin function signature to include birthday and explicitly position (for legacy compatibility) as updatable fields.
  • src/lib/firebase/storage-utils.ts
    • Added a new file containing isomorphic Firebase Storage utility functions, including STORAGE_LIMITS, ALLOWED_IMAGE_TYPES, validateFile, extractStoragePath, getRemainingStorage, and getStorageUsagePercentage.
  • src/lib/firebase/storage.ts
    • Refactored the file to re-export storage utility functions from storage-utils.ts.
    • Removed duplicated definitions of STORAGE_LIMITS, ALLOWED_IMAGE_TYPES, validateFile, extractStoragePath, getRemainingStorage, and getStorageUsagePercentage.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions
Copy link

Synthetic QA Passed!

Fake humans are happy. All critical user journeys working:

  • Parent login
  • Create player
  • View dashboard

Safe to merge.

@github-actions
Copy link

Deployed to staging: https://hustle-app-staging-d4f2hb75nq-uc.a.run.app

@github-actions
Copy link

Smoke tests ✅ Passed

Target: https://hustle-app-staging-d4f2hb75nq-uc.a.run.app

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several significant improvements to the application's stability and functionality. The server-side authentication has been stabilized by passing the NextRequest to auth calls, adding timeouts to prevent hangs, and implementing a fallback for user provisioning. The player update endpoint is now more robust, using Zod for validation and persisting additional profile fields like primary and secondary positions. Furthermore, the photo upload functionality has been corrected, and the user experience for managing positions has been enhanced. Overall, these are excellent changes that improve correctness, robustness, and maintainability. I've included a couple of minor suggestions to further refine the code.

primaryPosition: validatedData.primaryPosition,
position: validatedData.primaryPosition, // Legacy field for backward compatibility
secondaryPositions: validatedData.secondaryPositions ?? [],
positionNote: validatedData.positionNote?.trim() ? validatedData.positionNote.trim() : null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This line calls trim() twice, which is slightly inefficient. You can simplify this to validatedData.positionNote?.trim() || null, which achieves the same result more concisely and avoids the redundant call.

Suggested change
positionNote: validatedData.positionNote?.trim() ? validatedData.positionNote.trim() : null,
positionNote: validatedData.positionNote?.trim() || null,

Comment on lines +149 to +151
leagueOtherName: validatedData.leagueCode === 'other' && validatedData.leagueOtherName?.trim()
? validatedData.leagueOtherName.trim()
: null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for leagueOtherName is a bit verbose and calls trim() twice. It can be simplified into a more concise and efficient single line while maintaining the same logic.

      leagueOtherName: (validatedData.leagueCode === 'other' && validatedData.leagueOtherName?.trim()) || null,

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/api/storage/upload-player-photo/route.ts (1)

111-146: ⚠️ Potential issue | 🟠 Major

Delete order can lose the existing photo on partial failure.

Line 111 deletes the current photo before Line 137 uploads/persists the replacement. If upload or player update fails, the user can end up with no valid photo, and storage accounting can drift.

🔧 Proposed sequencing fix
-    // 7. Delete old photo if exists
-    let oldPhotoSizeMB = 0;
-    if (player.photoUrl) {
-      const oldPath = extractStoragePath(player.photoUrl);
-      if (oldPath) {
-        try {
-          oldPhotoSizeMB = await deletePlayerPhotoAdmin(oldPath);
-          logger.info('Replaced old player photo', {
-            userId,
-            playerId,
-            oldPath,
-            oldSize: oldPhotoSizeMB,
-          });
-        } catch (error) {
-          // Log but don't fail - old photo might already be deleted
-          logger.warn('Failed to delete old player photo', {
-            userId,
-            playerId,
-            oldPath,
-            error,
-          });
-        }
-      }
-    }
-
-    // 8. Upload new photo
-    const uploadResult = await uploadPlayerPhotoAdmin({ file, userId, playerId });
-
-    // 9. Update player photoUrl in Firestore
-    await updatePlayerAdmin(userId, playerId, {
-      photoUrl: uploadResult.url,
-    });
+    const oldPath = player.photoUrl ? extractStoragePath(player.photoUrl) : null;
+
+    // 7. Upload new photo first
+    const uploadResult = await uploadPlayerPhotoAdmin({ file, userId, playerId });
+    try {
+      // 8. Persist new URL
+      await updatePlayerAdmin(userId, playerId, { photoUrl: uploadResult.url });
+    } catch (error) {
+      // Compensate orphaned upload if Firestore update fails
+      await deletePlayerPhotoAdmin(uploadResult.path).catch(() => undefined);
+      throw error;
+    }
+
+    // 9. Delete old photo only after successful switch
+    let oldPhotoSizeMB = 0;
+    if (oldPath) {
+      try {
+        oldPhotoSizeMB = await deletePlayerPhotoAdmin(oldPath);
+      } catch (error) {
+        logger.warn('Failed to delete old player photo', { userId, playerId, oldPath, error });
+      }
+    }

     // 10. Update workspace storage usage
     const netStorageChange = uploadResult.size - oldPhotoSizeMB;
     await updateWorkspaceStorageUsageAdmin(workspace.id, netStorageChange);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/storage/upload-player-photo/route.ts` around lines 111 - 146, The
current flow deletes the old photo (deletePlayerPhotoAdmin) before uploading and
persisting the replacement, risking loss if the upload/update fails; change the
sequence to first upload the new file via uploadPlayerPhotoAdmin, then call
updatePlayerAdmin to persist photoUrl, and only after successful persistence
attempt to delete the old file via deletePlayerPhotoAdmin (using
extractStoragePath to find oldPath). Compute and call
updateWorkspaceStorageUsageAdmin with netStorageChange = uploadedSize -
deletedOldSize, and if deletion fails log and avoid subtracting old size;
additionally, if upload succeeds but updatePlayerAdmin fails, delete the newly
uploaded file to avoid orphaned storage (wrap deletes in try/catch and log).
🧹 Nitpick comments (4)
src/app/api/players/create/route.ts (1)

84-95: Consider validating the session cookie before verification.

The empty string fallback (|| '') means verifySessionCookie('') will be called if no cookie exists, which will throw an error caught by the try-catch. While functionally safe, an early return would be slightly cleaner and avoids an unnecessary Firebase call.

♻️ Optional: Add early validation
         // Get the decoded token claims from the current session
         const sessionCookie = request.cookies.get('__session')?.value || '';
+        if (!sessionCookie) {
+          throw new Error('No session cookie for fallback provisioning');
+        }
         const decodedToken = await adminAuth.verifySessionCookie(sessionCookie);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/players/create/route.ts` around lines 84 - 95, The code calls
adminAuth.verifySessionCookie even when no cookie exists because sessionCookie
is set to '' via the || fallback; change the flow to validate the cookie first
(check request.cookies.get('__session')?.value or a local sessionCookie variable
for truthiness) and if it's missing do an early return or skip the fallback
provisioning block, otherwise call adminAuth.verifySessionCookie(sessionCookie),
then proceed to ensureUserProvisioned and getUserProfileAdmin; update logging to
reflect the early-return path and keep references to request.cookies,
sessionCookie, adminAuth.verifySessionCookie, ensureUserProvisioned,
getUserProfileAdmin, and logger to locate the affected code.
src/lib/firebase/admin-services/players.ts (1)

172-194: Normalize position fields inside updatePlayerAdmin to enforce invariants.

Right now the service trusts callers to keep primaryPosition, legacy position, and secondaryPositions consistent. Centralizing normalization here avoids silent drift from any future caller.

♻️ Suggested normalization in `updatePlayerAdmin`
 export async function updatePlayerAdmin(
   userId: string,
   playerId: string,
   data: Partial<{
@@
   }>
 ): Promise<void> {
   try {
     const playerRef = adminDb.collection(`users/${userId}/players`).doc(playerId);
+    const normalizedData = { ...data };
+
+    if (normalizedData.primaryPosition) {
+      normalizedData.position = normalizedData.primaryPosition;
+      if (Array.isArray(normalizedData.secondaryPositions)) {
+        normalizedData.secondaryPositions = normalizedData.secondaryPositions.filter(
+          (pos) => pos !== normalizedData.primaryPosition
+        );
+      }
+    }
+
     await playerRef.update({
-      ...data,
+      ...normalizedData,
       updatedAt: new Date(),
     });
   } catch (error: any) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/firebase/admin-services/players.ts` around lines 172 - 194,
updatePlayerAdmin currently trusts callers to keep primaryPosition, legacy
position, and secondaryPositions consistent; centralize normalization inside
updatePlayerAdmin by computing/validating the position fields before calling
playerRef.update: ensure primaryPosition and position are synchronized (if one
is provided prefer primaryPosition or fallback deterministically to position),
dedupe secondaryPositions and remove any entry equal to the resolved
primary/position, normalize empty arrays/nulls to undefined when appropriate,
and only spread the sanitized primaryPosition, position, and secondaryPositions
into the update payload (alongside updatedAt) so invariants are enforced
regardless of caller input.
src/app/api/players/[id]/route.test.ts (1)

76-81: Add one regression case for primary/secondary overlap.

Nice expansion of payload/assertions. Consider one extra test where secondaryPositions contains the new primaryPosition to lock the position-consistency behavior this PR targets.

Also applies to: 252-267

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/players/`[id]/route.test.ts around lines 76 - 81, Add a
regression test in the existing players update tests to cover primary/secondary
overlap: craft an update payload where primaryPosition is e.g. 'ST' and
secondaryPositions includes that same value (e.g. ['ST','LW']), call the same
update helper used in the file (the PATCH/update test block in route.test.ts),
and assert that the response preserves the provided primaryPosition and does not
include that primaryPosition inside secondaryPositions (i.e.,
response.primaryPosition === 'ST' and response.secondaryPositions does not
contain 'ST'); add this case alongside the existing payload/assertion blocks
(also apply the same addition around the other corresponding test block at the
region mentioned).
src/app/api/players/route.ts (1)

36-45: Consider normalizing all optional profile fields for a stable response shape.

secondaryPositions, positionNote, and leagueOtherName are normalized, but gender, primaryPosition, and leagueCode may still serialize as omitted when undefined. Consistent null defaults can reduce client-side branching.

Optional response-shape normalization diff
         return {
           id: player.id,
           name: player.name,
           birthday: player.birthday,
-          gender: player.gender,
-          primaryPosition: player.primaryPosition,
+          gender: player.gender ?? null,
+          primaryPosition: player.primaryPosition ?? null,
           secondaryPositions: player.secondaryPositions ?? [],
           positionNote: player.positionNote ?? null,
           // Legacy field (backward compatibility)
           position: player.primaryPosition ?? player.position,
           teamClub: player.teamClub,
-          leagueCode: player.leagueCode,
+          leagueCode: player.leagueCode ?? null,
           leagueOtherName: player.leagueOtherName ?? null,
           photoUrl: player.photoUrl,
           pendingGames: unverifiedGames.length,
           parentEmail: parentUser?.email ?? null
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/players/route.ts` around lines 36 - 45, Normalize optional
profile fields in the response by ensuring gender, primaryPosition, and
leagueCode are explicitly set to null when undefined; update the object
construction that uses player.gender, player.primaryPosition, and
player.leagueCode (alongside the already-normalized secondaryPositions,
positionNote, and leagueOtherName) to use a null-default (e.g., player.gender ??
null) so the response shape remains stable for clients while preserving the
existing legacy position logic (position: player.primaryPosition ??
player.position).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/auth.ts`:
- Around line 74-78: The withTimeout helper is leaving timers active when the
wrapped promise rejects (used around adminAuth.verifySessionCookie and other
auth calls like the ones at lines 102-106, 108-112, 116-120), so update
src/lib/utils/timeout.ts to always clear the timeout on both resolve and reject
(e.g., clear the timer in a finally or ensure both then and catch handlers call
clearTimeout) so rejected auth paths don't accumulate stale timers; keep the
same API so calls using withTimeout( adminAuth.verifySessionCookie(...),
VERIFY_SESSION_COOKIE_TIMEOUT_MS, 'verifySessionCookie' ) continue to work.

---

Outside diff comments:
In `@src/app/api/storage/upload-player-photo/route.ts`:
- Around line 111-146: The current flow deletes the old photo
(deletePlayerPhotoAdmin) before uploading and persisting the replacement,
risking loss if the upload/update fails; change the sequence to first upload the
new file via uploadPlayerPhotoAdmin, then call updatePlayerAdmin to persist
photoUrl, and only after successful persistence attempt to delete the old file
via deletePlayerPhotoAdmin (using extractStoragePath to find oldPath). Compute
and call updateWorkspaceStorageUsageAdmin with netStorageChange = uploadedSize -
deletedOldSize, and if deletion fails log and avoid subtracting old size;
additionally, if upload succeeds but updatePlayerAdmin fails, delete the newly
uploaded file to avoid orphaned storage (wrap deletes in try/catch and log).

---

Nitpick comments:
In `@src/app/api/players/`[id]/route.test.ts:
- Around line 76-81: Add a regression test in the existing players update tests
to cover primary/secondary overlap: craft an update payload where
primaryPosition is e.g. 'ST' and secondaryPositions includes that same value
(e.g. ['ST','LW']), call the same update helper used in the file (the
PATCH/update test block in route.test.ts), and assert that the response
preserves the provided primaryPosition and does not include that primaryPosition
inside secondaryPositions (i.e., response.primaryPosition === 'ST' and
response.secondaryPositions does not contain 'ST'); add this case alongside the
existing payload/assertion blocks (also apply the same addition around the other
corresponding test block at the region mentioned).

In `@src/app/api/players/create/route.ts`:
- Around line 84-95: The code calls adminAuth.verifySessionCookie even when no
cookie exists because sessionCookie is set to '' via the || fallback; change the
flow to validate the cookie first (check request.cookies.get('__session')?.value
or a local sessionCookie variable for truthiness) and if it's missing do an
early return or skip the fallback provisioning block, otherwise call
adminAuth.verifySessionCookie(sessionCookie), then proceed to
ensureUserProvisioned and getUserProfileAdmin; update logging to reflect the
early-return path and keep references to request.cookies, sessionCookie,
adminAuth.verifySessionCookie, ensureUserProvisioned, getUserProfileAdmin, and
logger to locate the affected code.

In `@src/app/api/players/route.ts`:
- Around line 36-45: Normalize optional profile fields in the response by
ensuring gender, primaryPosition, and leagueCode are explicitly set to null when
undefined; update the object construction that uses player.gender,
player.primaryPosition, and player.leagueCode (alongside the already-normalized
secondaryPositions, positionNote, and leagueOtherName) to use a null-default
(e.g., player.gender ?? null) so the response shape remains stable for clients
while preserving the existing legacy position logic (position:
player.primaryPosition ?? player.position).

In `@src/lib/firebase/admin-services/players.ts`:
- Around line 172-194: updatePlayerAdmin currently trusts callers to keep
primaryPosition, legacy position, and secondaryPositions consistent; centralize
normalization inside updatePlayerAdmin by computing/validating the position
fields before calling playerRef.update: ensure primaryPosition and position are
synchronized (if one is provided prefer primaryPosition or fallback
deterministically to position), dedupe secondaryPositions and remove any entry
equal to the resolved primary/position, normalize empty arrays/nulls to
undefined when appropriate, and only spread the sanitized primaryPosition,
position, and secondaryPositions into the update payload (alongside updatedAt)
so invariants are enforced regardless of caller input.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e533740 and 69670b5.

📒 Files selected for processing (37)
  • eslint.config.mjs
  • src/app/api/account/pin/route.ts
  • src/app/api/billing/create-checkout-session/route.ts
  • src/app/api/debug/biometrics/[playerId]/route.ts
  • src/app/api/debug/workout-logs/[playerId]/route.ts
  • src/app/api/games/route.ts
  • src/app/api/players/[id]/assessments/[assessmentId]/route.ts
  • src/app/api/players/[id]/assessments/route.ts
  • src/app/api/players/[id]/biometrics/[logId]/route.ts
  • src/app/api/players/[id]/biometrics/route.ts
  • src/app/api/players/[id]/cardio-logs/[logId]/route.ts
  • src/app/api/players/[id]/cardio-logs/route.ts
  • src/app/api/players/[id]/dream-gym/ai-strategy/route.ts
  • src/app/api/players/[id]/dream-gym/check-in/route.ts
  • src/app/api/players/[id]/dream-gym/events/[eventId]/route.ts
  • src/app/api/players/[id]/dream-gym/events/route.ts
  • src/app/api/players/[id]/dream-gym/route.ts
  • src/app/api/players/[id]/dream-gym/workout-logs/[logId]/route.ts
  • src/app/api/players/[id]/dream-gym/workout-logs/route.ts
  • src/app/api/players/[id]/journal/[entryId]/route.ts
  • src/app/api/players/[id]/journal/route.ts
  • src/app/api/players/[id]/practice-logs/[logId]/route.ts
  • src/app/api/players/[id]/practice-logs/route.ts
  • src/app/api/players/[id]/route.test.ts
  • src/app/api/players/[id]/route.ts
  • src/app/api/players/create/route.ts
  • src/app/api/players/route.ts
  • src/app/api/storage/delete-player-photo/route.ts
  • src/app/api/storage/upload-player-photo/route.ts
  • src/app/api/verify/route.ts
  • src/app/dashboard/add-athlete/page.tsx
  • src/app/dashboard/athletes/[id]/edit/page.tsx
  • src/app/login/page.tsx
  • src/lib/auth.ts
  • src/lib/firebase/admin-services/players.ts
  • src/lib/firebase/storage-utils.ts
  • src/lib/firebase/storage.ts

Comment on lines +52 to 60
async function getSessionCookieFromHeaders(): Promise<string | null> {
const cookieStore = await cookies();
return cookieStore.get('__session')?.value || null;
return cookieStore.get(SESSION_COOKIE_NAME)?.value || null;
}

async function getSessionCookie(request?: NextRequest): Promise<string | null> {
if (request) return getSessionCookieFromRequest(request);
return getSessionCookieFromHeaders();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Authwithprofile not request-scoped 🐞 Bug ⛯ Reliability

Multiple API routes still call authWithProfile() without passing the NextRequest, causing
authWithProfile() to fall back to cookies() from next/headers. This likely reintroduces the same
class of cookie/request-stream instability the PR is trying to fix, leaving some authenticated
endpoints prone to hangs/timeouts.
Agent Prompt
## Issue description
Several API routes still call `authWithProfile()` without passing the `NextRequest`, which forces auth to fall back to `cookies()` from `next/headers`. This undermines the PR’s “read cookies from NextRequest” stabilization and may leave these routes vulnerable to the same hang/instability.

## Issue Context
`authWithProfile(request?: NextRequest)` is designed to use `request.cookies` when a request is provided and fall back to `cookies()` otherwise.

## Fix Focus Areas
- src/app/api/billing/change-plan/route.ts[32-42]
- src/app/api/billing/invoices/route.ts[27-50]
- src/app/api/billing/portal/route.ts[27-50]
- src/app/api/billing/create-portal-session/route.ts[24-35]
- src/app/api/admin/billing/replay-events/route.ts[106-116]
- src/app/api/workspace/current/route.ts[10-26]
- src/lib/auth.ts[52-60]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 28, 2026

Greptile Summary

Fixed authentication hangs by adding timeout protection and request-based cookie reading across all API routes, while extending player profiles to properly persist position data (primary, secondary, notes) and fixing photo uploads to use the correct endpoint with proper FormData keys.

Key Changes

  • Stabilized server auth with 10-15 second timeouts on Firebase operations and automatic user provisioning fallback
  • Fixed player update endpoint to validate with Zod schema and persist all position fields (primaryPosition, secondaryPositions, positionNote)
  • Corrected photo upload FormData key from 'photo' to 'file' and endpoint from /api/players/upload-photo to /api/storage/upload-player-photo
  • Refactored storage utilities into isomorphic storage-utils.ts module for safe client/server imports
  • Added position consistency logic to prevent primary position from appearing in secondary positions list
  • Updated 30+ API routes to pass request parameter to auth() for consistent cookie reading

Testing Coverage

  • Unit tests updated to match new validation schema and position persistence
  • Changed from clearAllMocks to resetAllMocks for better test isolation

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk - well-structured fixes with proper validation and test coverage
  • All changes are focused bug fixes with clear intent: auth timeout protection prevents hangs, position persistence matches schema validation, photo upload uses correct API contract, and storage refactoring maintains proper separation of concerns. Tests updated to match new behavior.
  • No files require special attention - changes are straightforward and well-tested

Important Files Changed

Filename Overview
src/lib/auth.ts Added timeout protection to Firebase auth operations, request-based cookie reading, and automatic user provisioning fallback to prevent sign-in hangs
src/app/api/players/[id]/route.ts Switched to Zod validation and now properly persists all player position fields (primary, secondary, positionNote) plus league and gender data
src/app/dashboard/add-athlete/page.tsx Fixed photo upload to use correct endpoint and FormData key ('file'), added logic to filter primary position from secondary positions on change
src/app/dashboard/athletes/[id]/edit/page.tsx Fixed photo upload endpoint and FormData key, added position consistency logic matching add-athlete page
src/lib/firebase/storage-utils.ts New isomorphic utility module for storage validation and limits, safely importable from both client and server
src/app/api/storage/upload-player-photo/route.ts Migrated to admin-storage helpers and admin-services for server-side storage operations, maintains proper access control
src/app/login/page.tsx Added timeout protection to getIdToken call to prevent indefinite hangs during authentication
src/app/api/players/[id]/route.test.ts Updated tests to reflect new validation schema and position persistence fields, changed to resetAllMocks for cleaner test isolation

Last reviewed commit: 69670b5

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

37 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Synthetic QA Passed!

Fake humans are happy. All critical user journeys working:

  • Parent login
  • Create player
  • View dashboard

Safe to merge.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLAUDE.md`:
- Around line 64-68: The documentation currently implies
03-Tests/e2e/.auth/user.json is a tracked file; update the CLAUDE.md E2E details
to state clearly that the authenticated storage state
(03-Tests/e2e/.auth/user.json) is generated at runtime by the global setup
script (03-Tests/e2e/global-setup.ts) and that the .auth/ directory is
git-ignored (created locally, not source-controlled), so developers must run the
global setup or tests to produce the file; reference global-setup.ts and the
.auth/user.json path in the text to make this explicit.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 69670b5 and 12a5c7a.

📒 Files selected for processing (1)
  • CLAUDE.md

@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Deployed to staging: https://hustle-app-staging-d4f2hb75nq-uc.a.run.app

@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Smoke tests ✅ Passed

Target: https://hustle-app-staging-d4f2hb75nq-uc.a.run.app

@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Synthetic QA Passed!

Fake humans are happy. All critical user journeys working:

  • Parent login
  • Create player
  • View dashboard

Safe to merge.

@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Deployed to staging: https://hustle-app-staging-d4f2hb75nq-uc.a.run.app

@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Smoke tests ✅ Passed

Target: https://hustle-app-staging-d4f2hb75nq-uc.a.run.app

@jeremylongshore jeremylongshore force-pushed the fix/signin-hang-positions branch from 7f605b5 to 783046d Compare March 2, 2026 03:48
@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Synthetic QA Passed!

Fake humans are happy. All critical user journeys working:

  • Parent login
  • Create player
  • View dashboard

Safe to merge.

@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Deployed to staging: https://hustle-app-staging-d4f2hb75nq-uc.a.run.app

@github-actions
Copy link

github-actions bot commented Mar 2, 2026

Smoke tests ✅ Passed

Target: https://hustle-app-staging-d4f2hb75nq-uc.a.run.app

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/api/storage/upload-player-photo/route.ts (1)

111-147: ⚠️ Potential issue | 🟠 Major

Do not delete the existing photo before new upload + DB update succeeds.

Current ordering can leave the player pointing to a deleted image (or leave orphaned state) when downstream steps fail.

🔧 Suggested sequencing change
-    // 7. Delete old photo if exists
-    let oldPhotoSizeMB = 0;
-    if (player.photoUrl) {
-      const oldPath = extractStoragePath(player.photoUrl);
-      if (oldPath) {
-        try {
-          oldPhotoSizeMB = await deletePlayerPhotoAdmin(oldPath);
-          logger.info('Replaced old player photo', {
-            userId,
-            playerId,
-            oldPath,
-            oldSize: oldPhotoSizeMB,
-          });
-        } catch (error) {
-          logger.warn('Failed to delete old player photo', {
-            userId,
-            playerId,
-            oldPath,
-            error,
-          });
-        }
-      }
-    }
-
-    // 8. Upload new photo
-    const uploadResult = await uploadPlayerPhotoAdmin({ file, userId, playerId });
-
-    // 9. Update player photoUrl in Firestore
-    await updatePlayerAdmin(userId, playerId, {
-      photoUrl: uploadResult.url,
-    });
+    // 7. Capture old photo path (delete only after successful replace)
+    const oldPath = player.photoUrl ? extractStoragePath(player.photoUrl) : null;
+
+    // 8. Upload new photo
+    const uploadResult = await uploadPlayerPhotoAdmin({ file, userId, playerId });
+
+    // 9. Persist new URL first
+    await updatePlayerAdmin(userId, playerId, { photoUrl: uploadResult.url });
+
+    // 10. Best-effort old photo cleanup after successful update
+    let oldPhotoSizeMB = 0;
+    if (oldPath) {
+      try {
+        oldPhotoSizeMB = await deletePlayerPhotoAdmin(oldPath);
+      } catch (error) {
+        logger.warn('Failed to delete old player photo', { userId, playerId, oldPath, error });
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/storage/upload-player-photo/route.ts` around lines 111 - 147, The
code currently deletes the old photo before the new upload and DB update,
risking broken references; instead, first call uploadPlayerPhotoAdmin(...) and
then updatePlayerAdmin(...) with uploadResult.url, only after those succeed
perform deletion of the old photo (use extractStoragePath(player.photoUrl) to
get oldPath) by calling deletePlayerPhotoAdmin(oldPath) and use the returned old
size to compute netStorageChange = uploadResult.size - oldPhotoSizeMB, then call
updateWorkspaceStorageUsageAdmin(workspace.id, netStorageChange); update logging
around upload, DB update, and deletion (functions: uploadPlayerPhotoAdmin,
updatePlayerAdmin, deletePlayerPhotoAdmin, updateWorkspaceStorageUsageAdmin,
extractStoragePath).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/firebase/storage.ts`:
- Around line 21-30: Update the re-exports that currently reference
'./storage-utils' to use the repo path alias (replace './storage-utils' with the
'@/...' alias) so the exported symbols STORAGE_LIMITS, ALLOWED_IMAGE_TYPES,
validateFile, extractStoragePath, getRemainingStorage, getStorageUsagePercentage
and the ValidationResult type are re-exported from the aliased module; locate
the export block that lists those symbols and change the module specifier to the
equivalent '@/...' import path per coding guidelines.

---

Outside diff comments:
In `@src/app/api/storage/upload-player-photo/route.ts`:
- Around line 111-147: The code currently deletes the old photo before the new
upload and DB update, risking broken references; instead, first call
uploadPlayerPhotoAdmin(...) and then updatePlayerAdmin(...) with
uploadResult.url, only after those succeed perform deletion of the old photo
(use extractStoragePath(player.photoUrl) to get oldPath) by calling
deletePlayerPhotoAdmin(oldPath) and use the returned old size to compute
netStorageChange = uploadResult.size - oldPhotoSizeMB, then call
updateWorkspaceStorageUsageAdmin(workspace.id, netStorageChange); update logging
around upload, DB update, and deletion (functions: uploadPlayerPhotoAdmin,
updatePlayerAdmin, deletePlayerPhotoAdmin, updateWorkspaceStorageUsageAdmin,
extractStoragePath).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f605b5 and 783046d.

📒 Files selected for processing (37)
  • CLAUDE.md
  • eslint.config.mjs
  • src/app/api/account/pin/route.ts
  • src/app/api/billing/create-checkout-session/route.ts
  • src/app/api/debug/biometrics/[playerId]/route.ts
  • src/app/api/debug/workout-logs/[playerId]/route.ts
  • src/app/api/games/route.ts
  • src/app/api/players/[id]/assessments/[assessmentId]/route.ts
  • src/app/api/players/[id]/assessments/route.ts
  • src/app/api/players/[id]/biometrics/[logId]/route.ts
  • src/app/api/players/[id]/biometrics/route.ts
  • src/app/api/players/[id]/cardio-logs/[logId]/route.ts
  • src/app/api/players/[id]/cardio-logs/route.ts
  • src/app/api/players/[id]/dream-gym/ai-strategy/route.ts
  • src/app/api/players/[id]/dream-gym/check-in/route.ts
  • src/app/api/players/[id]/dream-gym/events/[eventId]/route.ts
  • src/app/api/players/[id]/dream-gym/events/route.ts
  • src/app/api/players/[id]/dream-gym/route.ts
  • src/app/api/players/[id]/dream-gym/workout-logs/[logId]/route.ts
  • src/app/api/players/[id]/dream-gym/workout-logs/route.ts
  • src/app/api/players/[id]/journal/[entryId]/route.ts
  • src/app/api/players/[id]/journal/route.ts
  • src/app/api/players/[id]/practice-logs/[logId]/route.ts
  • src/app/api/players/[id]/practice-logs/route.ts
  • src/app/api/players/[id]/route.test.ts
  • src/app/api/players/[id]/route.ts
  • src/app/api/players/create/route.ts
  • src/app/api/players/route.ts
  • src/app/api/storage/delete-player-photo/route.ts
  • src/app/api/storage/upload-player-photo/route.ts
  • src/app/api/verify/route.ts
  • src/app/dashboard/add-athlete/page.tsx
  • src/app/dashboard/athletes/[id]/edit/page.tsx
  • src/lib/auth.ts
  • src/lib/firebase/admin-services/players.ts
  • src/lib/firebase/storage-utils.ts
  • src/lib/firebase/storage.ts
🚧 Files skipped from review as they are similar to previous changes (21)
  • src/app/api/players/[id]/dream-gym/ai-strategy/route.ts
  • src/app/api/players/[id]/dream-gym/route.ts
  • src/app/api/account/pin/route.ts
  • src/app/api/players/[id]/dream-gym/workout-logs/[logId]/route.ts
  • src/app/api/games/route.ts
  • src/lib/firebase/storage-utils.ts
  • src/app/dashboard/add-athlete/page.tsx
  • eslint.config.mjs
  • src/app/api/players/create/route.ts
  • src/app/api/verify/route.ts
  • src/app/api/billing/create-checkout-session/route.ts
  • src/app/api/players/[id]/assessments/[assessmentId]/route.ts
  • src/app/api/players/[id]/practice-logs/route.ts
  • src/app/api/players/[id]/dream-gym/workout-logs/route.ts
  • src/app/api/players/[id]/practice-logs/[logId]/route.ts
  • src/app/api/players/[id]/cardio-logs/route.ts
  • src/app/api/players/[id]/journal/[entryId]/route.ts
  • src/app/api/debug/biometrics/[playerId]/route.ts
  • src/app/api/players/[id]/cardio-logs/[logId]/route.ts
  • src/app/api/players/[id]/biometrics/route.ts
  • src/app/api/players/[id]/dream-gym/events/[eventId]/route.ts

Comment on lines +21 to 30
export {
STORAGE_LIMITS,
ALLOWED_IMAGE_TYPES,
validateFile,
extractStoragePath,
getRemainingStorage,
getStorageUsagePercentage,
} from './storage-utils';
export type { ValidationResult } from './storage-utils';

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use @/ alias for the new re-exports.

These re-exports currently use a relative path and should follow the repo import-path rule.

♻️ Suggested change
 export {
   STORAGE_LIMITS,
   ALLOWED_IMAGE_TYPES,
   validateFile,
   extractStoragePath,
   getRemainingStorage,
   getStorageUsagePercentage,
-} from './storage-utils';
-export type { ValidationResult } from './storage-utils';
+} from '@/lib/firebase/storage-utils';
+export type { ValidationResult } from '@/lib/firebase/storage-utils';

As per coding guidelines: **/*.{ts,tsx,js,jsx}: Use @/ path alias (maps to src/) for all imports.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/firebase/storage.ts` around lines 21 - 30, Update the re-exports that
currently reference './storage-utils' to use the repo path alias (replace
'./storage-utils' with the '@/...' alias) so the exported symbols
STORAGE_LIMITS, ALLOWED_IMAGE_TYPES, validateFile, extractStoragePath,
getRemainingStorage, getStorageUsagePercentage and the ValidationResult type are
re-exported from the aliased module; locate the export block that lists those
symbols and change the module specifier to the equivalent '@/...' import path
per coding guidelines.

@github-actions
Copy link

github-actions bot commented Mar 6, 2026

Deployed to staging: https://hustle-app-staging-d4f2hb75nq-uc.a.run.app

@github-actions
Copy link

github-actions bot commented Mar 6, 2026

Smoke tests ✅ Passed

Target: https://hustle-app-staging-d4f2hb75nq-uc.a.run.app

@jeremylongshore jeremylongshore force-pushed the fix/signin-hang-positions branch from 4d40b50 to 08a9549 Compare March 6, 2026 07:34
@github-actions
Copy link

github-actions bot commented Mar 6, 2026

Deployed to staging: https://hustle-app-staging-d4f2hb75nq-uc.a.run.app

@github-actions
Copy link

github-actions bot commented Mar 6, 2026

Smoke tests ✅ Passed

Target: https://hustle-app-staging-d4f2hb75nq-uc.a.run.app

jeremylongshore and others added 2 commits March 6, 2026 01:41
…cations

Document missing integration test commands and config, add mobile app
stack details (Expo Router, NativeWind, React Query), fix unit test
location accuracy, and add minor missing details.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jeremylongshore jeremylongshore force-pushed the fix/signin-hang-positions branch from 08a9549 to 2fb3d95 Compare March 6, 2026 07:41
@jeremylongshore jeremylongshore merged commit 275bd7e into main Mar 6, 2026
5 checks passed
@jeremylongshore jeremylongshore deleted the fix/signin-hang-positions branch March 6, 2026 07:41
@github-actions
Copy link

github-actions bot commented Mar 6, 2026

Deployed to staging: https://hustle-app-staging-d4f2hb75nq-uc.a.run.app

@github-actions
Copy link

github-actions bot commented Mar 6, 2026

Smoke tests ✅ Passed

Target: https://hustle-app-staging-d4f2hb75nq-uc.a.run.app

@coderabbitai coderabbitai bot mentioned this pull request Mar 6, 2026
5 tasks
jeremylongshore added a commit that referenced this pull request Mar 9, 2026
* fix(firebase): prevent sign-in hangs and fix athlete positions

* docs(CLAUDE.md): add integration tests, mobile stack, and fix test locations

Document missing integration test commands and config, add mobile app
stack details (Expo Router, NativeWind, React Query), fix unit test
location accuracy, and add minor missing details.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ci: disable all cron schedules to stop burning Actions minutes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: jeremylongshore <noreply@localhost.local>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant